package lab.cadl.lirui.markdown.core.parser

import guru.nidi.graphviz.engine.Format
import guru.nidi.graphviz.engine.Graphviz
import lab.cadl.lirui.markdown.core.exceptions.MarkdownException
import lab.cadl.lirui.markdown.core.model.*
import lab.cadl.lirui.markdown.core.utils.Escaping
import lab.cadl.lirui.markdown.core.utils.ImageCacheImpl
import lab.cadl.lirui.markdown.core.utils.calcIndent
import lab.cadl.lirui.markdown.core.utils.trimIndent
import net.sourceforge.plantuml.SourceStringReader
import java.io.File
import java.net.URI
import java.net.URISyntaxException
import java.util.*

object StandardPriority {
    const val HIGHEST: Int = Int.MAX_VALUE
    const val LOWEST: Int = Int.MIN_VALUE
    const val DEFAULT: Int = 0
}

interface BlockProcessor {
    val priority: Int
    fun test(block: List<String>, state: ParseState): TestResult
    fun process(block: List<String>, state: ParseState)
}

enum class TestState {
    ACCEPT,
    DECLINE,
    NEED_MORE,
}

class TestResult(val state: TestState, val block: List<String>, val remain: List<String>) {
    val accept: Boolean
        get() = state == TestState.ACCEPT

    companion object {
        fun fullAccept(block: List<String>): TestResult {
            return TestResult(TestState.ACCEPT, block.subList(0, block.size), emptyList())
        }

        fun accept(block: List<String>, remain: List<String>): TestResult {
            return TestResult(TestState.ACCEPT, block, remain)
        }

        fun acceptSingleLine(block: List<String>): TestResult {
            return TestResult(TestState.ACCEPT, block.subList(0, 1), block.subList(1, block.size))
        }

        fun decline(block: List<String>): TestResult {
            return TestResult(TestState.DECLINE, emptyList(), block.subList(0, block.size))
        }

        fun needMore(block: List<String>): TestResult {
            return TestResult(TestState.NEED_MORE, block.subList(0, block.size), emptyList())
        }
    }
}

abstract class AbstractBlockProcessor(override val priority: Int) : BlockProcessor

class HeadingProcessor : AbstractBlockProcessor(StandardPriority.HIGHEST) {
    private val pattern = Regex("""(#+)\s+(.+)""")
    private val refPattern = Regex("""\[([^]]+)](?:\[([^]]*)])?\s*""")

    override fun test(block: List<String>, state: ParseState): TestResult {
        return if (pattern.matches(block[0])) {
            if (block.size >= 2 && refPattern.matches(block[1])) {
                TestResult.accept(block.subList(0, 2), block.subList(2, block.size))
            } else {
                TestResult.acceptSingleLine(block)
            }
        } else {
            TestResult.decline(block)
        }
    }

    override fun process(block: List<String>, state: ParseState) {
        assert(block.size <= 2)

        val match = pattern.matchEntire(block[0])!!
        val level = match.groupValues[1].length
        val title = match.groupValues[2].trim()

        val section = when (title) {
            "ABSTRACT" -> state.appendTopLevelSection(AbstractSection(parent = state.document, isChinese = false))
            "摘要" -> state.appendTopLevelSection(AbstractSection(parent = state.document, isChinese = true))
            else -> state.createSection(level, title)
        }

        if (block.size == 2) {
            val refMatch = refPattern.matchEntire(block[1])!!
            section.refId = refMatch.groupValues[1].trim()

            val refLabel = refMatch.groupValues[2].trim()
            if (refLabel != "") {
                section.refLabel = refLabel
            }

            state.document.addTarget(section)
        }
    }
}

abstract class SingleLineBlockProcessor(val pattern: Regex, priority: Int = StandardPriority.DEFAULT) : AbstractBlockProcessor(priority) {
    override fun test(block: List<String>, state: ParseState): TestResult {
        return if (pattern.matches(block[0])) {
            TestResult.acceptSingleLine(block)
        } else {
            TestResult.decline(block)
        }
    }

    override fun process(block: List<String>, state: ParseState) {
        doProcess(pattern.matchEntire(block[0])!!, state)
    }

    abstract fun doProcess(matchEntire: MatchResult, state: ParseState)
}

class ParagraphProcessor : AbstractBlockProcessor(StandardPriority.LOWEST) {
    override fun test(block: List<String>, state: ParseState): TestResult {
        return TestResult.fullAccept(block)
    }

    override fun process(block: List<String>, state: ParseState) {
        val paragraph = Paragraph(parent = state.activeSection)
        state.activeSection.appendChild(paragraph)

        state.inlineProcessor.parse(block, paragraph)
    }
}

class KeyWordProcessor : AbstractBlockProcessor(StandardPriority.DEFAULT) {
    val startPattern = Regex("""\{\{keyword}}""")
    val stopPattern = Regex("""\{\{/keyword}}""")
    override fun test(block: List<String>, state: ParseState): TestResult {
        return testBalanceBlock(block, state, startPattern, stopPattern)
    }

    override fun process(block: List<String>, state: ParseState) {
        val keywords = block.subList(1, block.size - 1).map { it.trim() }
        val keyWorBlock = KeyWordBlock(state.activeSection, keywords)
        state.activeSection.appendChild(keyWorBlock)
    }
}

class BlockImageProcessor : AbstractBlockProcessor(StandardPriority.DEFAULT) {
    private val pattern = Regex("""!\[([^]]+)]\(([^)"]+)(?:"([^)"]+)")?\)""")

    override fun test(block: List<String>, state: ParseState): TestResult {
        return if (block.size == 1 && pattern.matches(block[0])) {
            TestResult.fullAccept(block)
        } else {
            TestResult.decline(block)
        }
    }

    override fun process(block: List<String>, state: ParseState) {
        val result = pattern.matchEntire(block[0])!!
        val refId = result.groupValues[1]
        var url = Escaping.unescaping(result.groupValues[2].trim())
        if (isLocalFile(url)) {
            url = File(state.imageCache.baseDir).resolve(url).toString()
        }

        val title = result.groupValues[3]
        val imageBlock = ImageBlock(parent = state.activeSection,
                title = if (title.isBlank()) {
                    refId
                } else {
                    title
                },
                url = url,
                refId = refId)
        state.activeSection.appendChild(imageBlock)
    }

    private fun isLocalFile(s: String): Boolean {
        return try {
            val uri = URI(s)
            uri.scheme == null || uri.scheme == "file"
        } catch (ex: URISyntaxException) {
            true
        }
    }
}

fun testBalanceBlock(block: List<String>, state: ParseState, startPattern: Regex, stopPattern: Regex): TestResult {
    if (block.isEmpty() || !startPattern.matches(block[0])) {
        return TestResult.decline(block)
    }

    var endIndex = 1
    var found = false
    while (endIndex < block.size) {
        val line = block[endIndex]
        if (stopPattern.matches(line)) {
            endIndex += 1
            found = true
            break
        } else {
            endIndex += 1
        }
    }

    return if (found) {
        TestResult.accept(block.subList(0, endIndex), block.subList(endIndex, block.size))
    } else {
        TestResult.needMore(block)
    }
}

class FencedCodeProcessor : AbstractBlockProcessor(StandardPriority.DEFAULT) {
    private val startPattern = Regex("""^```(\w+)?\s*(?:\[([^]]+)](?:\[([^]]+)])?)?\s*$""")
    private val stopPattern = Regex("""^```\s*$""")

    private val ioPatterm = Regex("""@([IO])\s+([^:]+):(.*)""")

    override fun test(block: List<String>, state: ParseState): TestResult {
        return testBalanceBlock(block, state, startPattern, stopPattern)
    }

    override fun process(block: List<String>, state: ParseState) {
        val match = startPattern.matchEntire(block[0])
        val lang = match!!.groupValues[1]
        val title = match.groupValues[2]
        val refId = if (match.groupValues[3].isBlank()) {
            title
        } else {
            match.groupValues[3]
        }

        val content = block.subList(1, block.size - 1).joinToString("\n")
        when (lang) {
            "graphviz" -> generateImage(refId, title, content, state, this::generateGraphviz)
            "plantuml" -> generateImage(refId, title, content, state, this::generatePlantUml)
            "sequence" -> generateImage(refId, title, "@startuml\n$content\n@enduml", state, this::generatePlantUml)
            "algorithm" -> {
                val algorithmBlock = AlgorithmBlock(parent = state.activeSection, title = title, refId = refId)
                for (line in block.subList(1, block.size - 1)) {
                    val ioMatch = ioPatterm.matchEntire(line)
                    if (ioMatch == null) {
                        // code line
                        val algorithmLine = AlgorithmLine(parent = algorithmBlock)
                        algorithmBlock.lines.add(algorithmLine)

                        state.inlineProcessor.parse(line, algorithmLine)
                    } else {
                        // meta line
                        val type = ioMatch.groupValues[1]
                        val name = ioMatch.groupValues[2].trim()
                        val desc = ioMatch.groupValues[3].trim()

                        val meta: AlgorithmMeta = when (type) {
                            "I" -> {
                                AlgorithmInput(parent = algorithmBlock, name = name)
                            }
                            "O" -> {
                                AlgorithmOutput(parent = algorithmBlock, name = name)
                            }
                            else -> throw MarkdownException("Unknown algorithm meta type $type")
                        }

                        state.inlineProcessor.parse(desc, meta)
                        algorithmBlock.metaList.add(meta)
                    }
                }

                state.activeSection.appendChild(algorithmBlock)
            }
            else -> {
                val blockElement = FencedCodeBlock(parent = state.activeSection, lang = lang, content = content,
                        indent = 0, title = title, refId = refId)
                state.activeSection.appendChild(blockElement)
            }
        }
    }

    private fun generateImage(refId: String, title: String, content: String, state: ParseState, generator: (content: String, file: File) -> Unit) {
        val file = state.imageCache.registerImage(content, ImageCacheImpl.DEFAULT_EXT)
        generator(content, file)
        val imageBlock = ImageBlock(parent = state.activeSection,
                title = title,
                // cache with input as url
                url = content,
                refId = refId)
        state.activeSection.appendChild(imageBlock)
    }

    private fun generatePlantUml(content: String, file: File) {
        val reader = SourceStringReader(content)
        reader.outputImage(file.outputStream())
    }

    private fun generateGraphviz(content: String, file: File) {
        val graph = Graphviz.fromString(content)
        graph.render(Format.PNG).toFile(file)
    }
}

class TableCellState(private val cell: TableCell, private val parentState: ParseState) : ParseState by parentState {
    override val activeSection: Section
        get() = cell.nestDocument
    override val cellContentParser: DocumentLinesParser
        get() = throw MarkdownException("cell content parser cannot be nested")

    override fun createSection(level: Int, title: String): Section {
        throw MarkdownException("could not create section in table cell")
    }
}

class TableBlockProcessor : AbstractBlockProcessor(StandardPriority.DEFAULT) {
    private val startPattern = Regex("""\{\{table(?:\[([^]]+)])?(?:<([lLrRbBmM]+)>)?}}\s+(.*)""")
    private val stopPattern = Regex("""\{\{/table}}""")
    private val entryPattern = Regex("""(\s*)[-*]\s*(.*)""")
    private val header = "HEADER"

    override fun test(block: List<String>, state: ParseState): TestResult {
        return testBalanceBlock(block, state, startPattern, stopPattern)
    }

    override fun process(block: List<String>, state: ParseState) {
        val match = startPattern.matchEntire(block[0])!!
        val refId = match.groupValues[1]
        val alignChars = match.groupValues[2].trim()
        val title = match.groupValues[3].trim()

        val align = createAlign(alignChars)

        val table = TableBlock(parent = state.activeSection, title = title, refId = refId)
        state.activeSection.appendChild(table)

        var row: TableRow? = null
        var cell: TableCell? = null

        val contentLines = block.subList(1, block.size - 1)
        var cellIndex = 0
        val cellLines: MutableList<String> = mutableListOf()
        var prevIndent = 0
        for (line in contentLines) {
            val matchEntry = entryPattern.matchEntire(line)
            if (matchEntry != null) {
                val isRow = matchEntry.groupValues[1].isEmpty()
                val indent = calcIndent(matchEntry.groupValues[1])
                val content = matchEntry.groupValues[2]

                if (isRow) {
                    if (cell != null) {
                        parseCellLines(cell, cellLines, state)
                    }

                    row = TableRow(parent = table, isHeader = content.trim().toUpperCase() == header)
                    cell = null
                    cellIndex = 0
                    prevIndent = 0
                } else {
                    if (prevIndent == 0 || indent == prevIndent) {
                        if (row == null) {
                            throw MarkdownException("cell input before row")
                        }

                        prevIndent = indent

                        if (cell != null) {
                            parseCellLines(cell, cellLines, state)
                        }

                        cell = TableCell(parent = row, horizonAlign = align(cellIndex), indent = indent)
                        cellIndex += 1

                        cellLines.clear()
                        if (!content.isBlank()) {
                            cellLines.add(content)
                        }
                    } else {
                        if (cellLines.size == 1 && !cellLines[0].trim().startsWith("-")) {
                            cellLines.add("")
                        }

                        cellLines.add(trimIndent(line, cell!!.indent + 4))
                    }
                }
            } else if (row != null) {
                if (cell == null) {
                    throw MarkdownException("continue input before new cell entry: $line")
                }

                cellLines.add(trimIndent(line, cell.indent + 4))
            } else {
                // ignore leading text
            }
        }

        if (cell != null) {
            parseCellLines(cell, cellLines, state)
        }
    }

    private fun parseCellLines(cell: TableCell, lines: List<String>, parentState: ParseState) {
        val cellState = TableCellState(cell, parentState)
        parentState.cellContentParser.parse(lines.iterator(), cellState)
        cell.moveChildren()
    }

    private fun createAlign(alignChars: String): (index: Int) -> HorizonAlign {
        val alignList = alignChars.map {
            when (it.toLowerCase()) {
                'l' -> HorizonAlign.LEFT
                'r' -> HorizonAlign.RIGHT
                'm' -> HorizonAlign.MIDDLE
                'b' -> HorizonAlign.BOTH
                else -> throw MarkdownException("unknown table align $it")
            }
        }

        val align = { index: Int ->
            if (index < alignList.size) {
                alignList[index]
            } else {
                HorizonAlign.BOTH
            }
        }

        return align
    }
}

class StandardTableProcessor : AbstractBlockProcessor(StandardPriority.DEFAULT) {
    private val splitterPattern = Regex("""\|(\s*:?-+:?\s*\|)+\s*""")
    private val refPattern = Regex("""^\[([^]]+)](?:\[([^]]+)])?\s*$""")

    override fun test(block: List<String>, state: ParseState): TestResult {
        if (block.size < 2) {
            return TestResult.decline(block)
        }

        if (!block[0].startsWith("|") || !splitterPattern.matches(block[1])) {
            return TestResult.decline(block)
        }
        var endIndex = 2
        while (endIndex < block.size && (block[endIndex].startsWith("|") || block[endIndex].startsWith("["))) {
            endIndex += 1
        }

        return TestResult.accept(block.subList(0, endIndex), block.subList(endIndex, block.size))
    }

    override fun process(block: List<String>, state: ParseState) {
        val alignments: List<HorizonAlign> = block[1].trim().trim('|').split('|').map { it.trim() }.map {
            if (it.startsWith(":")) {
                if (it.endsWith(":")) {
                    HorizonAlign.MIDDLE
                } else {
                    HorizonAlign.LEFT
                }
            } else {
                if (it.endsWith(":")) {
                    HorizonAlign.RIGHT
                } else {
                    HorizonAlign.BOTH
                }
            }
        }

        val align = { index: Int ->
            if (index < alignments.size) {
                alignments[index]
            } else {
                HorizonAlign.BOTH
            }
        }

        val table = TableBlock(parent = state.activeSection, title = "", refId = "")
        state.activeSection.appendChild(table)

        val headerRow = TableRow(parent = table, isHeader = true)
        parseCells(block[0], headerRow, state, align)

        for (line in block.subList(2, block.size)) {
            val match = refPattern.matchEntire(line)
            if (match == null) {
                val row = TableRow(parent = table, isHeader = false)
                parseCells(line, row, state, align)
            } else {
                table.title = match.groupValues[1]
                table.refId = if (match.groupValues[2].isBlank()) {
                    match.groupValues[1]
                } else {
                    match.groupValues[2]
                }
            }
        }
    }

    private fun parseCells(line: String, row: TableRow, state: ParseState, align: (i: Int) -> HorizonAlign) {
        val parts = line.trim().trim('|')
                .split("|").map { it.trim() }
        parts.forEachIndexed { index, part ->
            val cell = TableCell(parent = row, horizonAlign = align(index))
            state.inlineProcessor.parse(part, cell)
        }

    }
}

class ListBlockProcessor : AbstractBlockProcessor(StandardPriority.DEFAULT) {
    private val itemPattern = Regex("""([ \t]*)([-*]|(?:\d+\.)) (.*)""")

    override fun test(block: List<String>, state: ParseState): TestResult {
        assert(!block.isEmpty())

        var index = 0
        while (index < block.size && itemPattern.matches(block[index])) {
            index += 1
        }

        return if (index == 0) {
            TestResult.decline(block)
        } else {
            TestResult.accept(block.subList(0, index), block.subList(index, block.size))
        }
    }

    override fun process(block: List<String>, state: ParseState) {
        val blockStack: Stack<AbstractListBlock> = Stack()

        for (line in block) {
            val match = itemPattern.matchEntire(line) ?: throw MarkdownException("list contains none item line: $line")

            val indent = calcIndent(match.groupValues[1])
            val prefix = match.groupValues[2]
            val content = match.groupValues[3]

            when {
                blockStack.isEmpty() -> {
                    val list = createList(prefix, indent, 0, state.activeSection)
                    state.activeSection.appendChild(list)
                    blockStack.push(list)
                }
                blockStack.peek().indent < indent -> {
                    val parentList = blockStack.peek()
                    val list = createList(prefix, indent, parentList.level + 1, parentList)
                    parentList.appendChild(list)
                    blockStack.push(list)
                }
                blockStack.peek().indent > indent -> while (blockStack.peek().indent > indent) {
                    blockStack.pop()

                    if (blockStack.peek().indent < indent) {
                        val parentList = blockStack.peek()
                        val list = createList(prefix, indent, parentList.level + 1, parentList)
                        parentList.appendChild(list)
                        blockStack.push(list)
                    }
                }
            }

            val parent = blockStack.peek()
            val item = ListItem(parent = parent, index = parent.currentIndex)
            parent.currentIndex += 1

            blockStack.peek().appendChild(item)

            state.inlineProcessor.parse(content, item)
        }
    }

    private fun createList(prefix: String, indent: Int, level: Int, parent: Block): AbstractListBlock {
        return if (prefix[0].isDigit()) {
            val number = prefix.split('.')[0].toInt()
            val startNumber = if (number > 0) {
                number
            } else {
                1
            }
            OrderedList(parent = parent, indent = indent, startNumber = startNumber, level = level)
        } else {
            BulletList(parent = parent, indent = indent, marker = prefix, level = level)
        }
    }

    private fun calcIndent(prefixSpace: String): Int {
        return prefixSpace.map {
            when (it) {
                ' ' -> 1
                '\t' -> 4
                else -> 0
            }
        }.sum()
    }
}

class BlockQuoteProcessor : AbstractBlockProcessor(StandardPriority.DEFAULT) {
    val linePattern = Regex(""">\s+(?:\{\{([^}]+)}})?\s*(.*)""")

    override fun test(block: List<String>, state: ParseState): TestResult {
        val lines = mutableListOf<String>()
        for (line in block) {
            if (linePattern.matches(line)) {
                lines.add(line)
            } else {
                break
            }
        }

        return if (lines.isEmpty()) {
            TestResult.decline(block)
        } else {
            TestResult.accept(lines, block.subList(lines.size, block.size))
        }
    }

    override fun process(block: List<String>, state: ParseState) {
        var source = ""
        val contentLines = mutableListOf<String>()
        for (line in block) {
            val match = linePattern.matchEntire(line)!!
            val lineSource = match.groupValues[1]
            val content = match.groupValues[2]

            if (!lineSource.isBlank()) {
                if (!source.isBlank()) {
                    newBlockQuote(source, contentLines, state)
                    contentLines.clear()
                }

                source = lineSource
            }

            contentLines.add(content)
        }

        if (!contentLines.isEmpty()) {
            newBlockQuote(source, contentLines, state)
        }
    }

    private fun newBlockQuote(source: String, contentLines: List<String>, state: ParseState) {
        val blockQuote = BlockQuote(parent = state.activeSection, source = if (source.isBlank()) {
            null
        } else {
            source
        })
        state.activeSection.appendChild(blockQuote)

        state.inlineProcessor.parse(contentLines, blockQuote)
    }
}

class FormulaBlockProcessor : AbstractBlockProcessor(StandardPriority.DEFAULT) {
    private val pattern = Regex("""\$\$((?:[^$]|\\$)+)\$\$\s*(?:\[([^]]+)]\[([^]]*)])?\s*""")

    override fun test(block: List<String>, state: ParseState): TestResult {
        return if (pattern.matches(block[0])) {
            TestResult.accept(block.subList(0, 1), block.subList(1, block.size))
        } else {
            TestResult.decline(block)
        }
    }

    override fun process(block: List<String>, state: ParseState) {
        assert(block.size == 1)
        val match = pattern.matchEntire(block[0])!!
        val formulaText = match.groupValues[1]
        val title = match.groupValues[2]
        val refId = if (match.groupValues[3].isBlank()) {
            title
        } else {
            match.groupValues[3]
        }

        val formula = Formula(null, formulaText)
        val formulaBlock = FormulaBlock(state.activeSection, formula, title, refId)
        state.activeSection.appendChild(formulaBlock)
    }
}

class RefSourceProcessor : AbstractBlockProcessor(StandardPriority.DEFAULT) {
    private val pattern = Regex("""\{\{ref-source\s+([^}]+)}}\s*""")

    override fun test(block: List<String>, state: ParseState): TestResult {
        return if (pattern.matches(block[0])) {
            TestResult.acceptSingleLine(block)
        } else {
            TestResult.decline(block)
        }
    }

    override fun process(block: List<String>, state: ParseState) {
        val match = pattern.matchEntire(block[0])!!
        val path = match.groupValues[1].trim()

        val fullPath = state.resolveRelativePath(path)
        val file = File(fullPath)
        if (!file.exists()) {
            throw MarkdownException("Ref source [$fullPath] not found")
        }

        file.inputStream().use {
            PaperRefLoaderRegistry.findLoader(file).load(it, state.paperRefDatabase)
        }
    }
}

class RefShowProcessor : AbstractBlockProcessor(StandardPriority.DEFAULT) {
    private val pattern = Regex("""\{\{ref-show}}\s*""")

    override fun test(block: List<String>, state: ParseState): TestResult {
        return if (pattern.matches(block[0])) {
            TestResult.acceptSingleLine(block)
        } else {
            TestResult.decline(block)
        }
    }

    override fun process(block: List<String>, state: ParseState) {
        state.document.appendChild(RefSection(state.document))
    }
}

class IncludeBlockProcessor : AbstractBlockProcessor(StandardPriority.DEFAULT) {
    private val pattern = Regex("""\{\{include\s+([^}]+)}}\s*""")

    override fun test(block: List<String>, state: ParseState): TestResult {
        return if (pattern.matches(block[0])) {
            TestResult.acceptSingleLine(block)
        } else {
            TestResult.decline(block)
        }
    }

    override fun process(block: List<String>, state: ParseState) {
        val match = pattern.matchEntire(block[0])!!
        val filePath = match.groupValues[1].trim()
        val fullPath = state.resolveRelativePath(filePath)

        state.createChildProcessor().parse(File(fullPath))
    }
}

class TocBlockProcessor : SingleLineBlockProcessor(Regex("""\{\{toc}}""")) {
    override fun doProcess(matchEntire: MatchResult, state: ParseState) {
        state.appendTopLevelSection(TocBlock(parent = state.document))
    }
}

class ThematicBreakProcessor : SingleLineBlockProcessor(Regex("""----*\s*""")) {
    override fun doProcess(matchEntire: MatchResult, state: ParseState) {
        state.activeSection.appendChild(ThematicBreak(parent = state.activeSection))
    }
}

class PropBlockProcessor : SingleLineBlockProcessor(Regex("""\{\{prop\s+(\S+)\s+(.+)\s*}}""")) {
    override fun doProcess(matchEntire: MatchResult, state: ParseState) {
        val name = matchEntire.groupValues[1].trim()
        val value = matchEntire.groupValues[2].trim()
        state.activeSection.prop(name, value)
    }
}

class TitlePropProcessor : SingleLineBlockProcessor(Regex("""\{\{title\s+(.+)\s*}}""")) {
    override fun doProcess(matchEntire: MatchResult, state: ParseState) {
        val title = matchEntire.groupValues[1].trim()
        state.document.prop(MarkdownConstants.TitleProperty, title)
    }
}

class SubTitlePropProcessor : SingleLineBlockProcessor(Regex("""\{\{subTitle\s+(.+)\s*}}""")) {
    override fun doProcess(matchEntire: MatchResult, state: ParseState) {
        val subTitle = matchEntire.groupValues[1].trim()
        state.document.prop(MarkdownConstants.SubTitleProperty, subTitle)
    }
}

class VariableBlockProcessor : SingleLineBlockProcessor(Regex("""\{\{var\s+([\d\w_\-$]+)\s+(.+)\s*}}""")) {
    override fun doProcess(matchEntire: MatchResult, state: ParseState) {
        val name = matchEntire.groupValues[1].trim()
        val value = matchEntire.groupValues[2].trim()
        state.activeSection.updateVariable(name, value)
    }
}